package org.codehaus.mojo.l10n;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;

import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.model.Resource;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.codehaus.plexus.util.DirectoryScanner;

/**
 * A simple report for keeping track of l10n status. It lists all bundle
 * properties files and the number of properties in them. For a configurable
 * list of locales it also tracks the progress of localization.
 * 
 * @author <a href="mkleint@codehaus.org">Milos Kleint</a>
 * @goal report
 */
public class L10NStatusReport extends AbstractMavenReport {

	/**
	 * Report output directory.
	 * 
	 * @parameter default-value="${project.build.directory}/generated-site/xdoc"
	 */
	private File outputDirectory;

	/**
	 * Doxia Site Renderer.
	 * 
	 * @component
	 */
	private Renderer siteRenderer;

	/**
	 * A list of locale strings that are to be watched for l10n status.
	 * 
	 * @parameter
	 */
	protected List<String> locales;

	/**
	 * The Maven Project.
	 * 
	 * @parameter expression="${project}"
	 * @required
	 * @readonly
	 */
	private MavenProject project;

	/**
	 * The list of resources that are scanned for properties bundles.
	 * 
	 * @parameter default-value="${project.resources}"
	 * @readonly
	 */
	private List<Resource> resources;

	/**
	 * A list of exclude patterns to use. By default no files are excluded.
	 * 
	 * @parameter
	 */
	private List<String> excludes;

	/**
	 * A list of include patterns to use. By default all
	 * <code>*.properties</code> files are included.
	 * 
	 * @parameter
	 */
	private List<String> includes;

	/**
	 * The projects in the reactor for aggregation report.
	 * 
	 * @parameter expression="${reactorProjects}"
	 * @readonly
	 */
	private List<MavenProject> reactorProjects;

	/**
	 * Whether to build an aggregated report at the root, or build individual
	 * reports.
	 * 
	 * @parameter expression="${maven.l10n.aggregate}" default-value="false"
	 */
	private boolean aggregate;

	/**
	 * Create a table of all localization values.
	 * 
	 * @parameter expression="${maven.l10n.details}" default-value="true"
	 */
	protected boolean details;

	private static final String[] DEFAULT_INCLUDES = { "**/*.properties" };
	private static final String[] EMPTY_STRING_ARRAY = {};
	private Map<String, L10n> l10ns;
	private L10NStatusRenderer renderer;
	private int[] keyTotals;
	private int[] missingKeyTotals;

	private List<String> localeStrings;

	public boolean canGenerateReport() {
		return canGenerateReport(constructResourceDirs());
	}

	/**
	 * @param sourceDirs
	 * @return true if the report can be generated
	 */
	protected boolean canGenerateReport(
			Map<MavenProject, List<Resource>> sourceDirs) {
		boolean canGenerate = !sourceDirs.isEmpty();

		if (aggregate && !project.isExecutionRoot()) {
			canGenerate = false;
		}
		return canGenerate;
	}

	/**
	 * Collects resource definitions from all projects in reactor.
	 * 
	 * @return
	 */
	protected Map<MavenProject, List<Resource>> constructResourceDirs() {
		Map<MavenProject, List<Resource>> sourceDirs = new HashMap<MavenProject, List<Resource>>();
		if (aggregate) {
			for (MavenProject project : reactorProjects) {
				if (project.getResources() != null && !project.getResources().isEmpty()) {
					sourceDirs.put(project,
							new ArrayList<Resource>(project.getResources()));
				}

			}
		} else {
			if (resources != null && !resources.isEmpty()) {
				sourceDirs.put(project, new ArrayList<Resource>(resources));
			}
		}
		return sourceDirs;
	}

	/**
	 * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale)
	 */
	protected void executeReport(Locale reportLocale)
			throws MavenReportException {
		l10ns = new HashMap<String, L10n>();
		Map<MavenProject, List<Resource>> res = constructResourceDirs();
		for (MavenProject prj : res.keySet()) {
			getLog().debug("Project: " +prj.getArtifactId());
			List<Resource> lst = res.get(prj);
			for (Resource resource : lst) {
				getLog().debug("Resource dir:" + resource.getDirectory());

				File resourceDirectory = new File(resource.getDirectory());

				if (!resourceDirectory.exists()) {
					getLog().info(
							"Resource directory does not exist: "
									+ resourceDirectory);
					continue;
				}

				DirectoryScanner scanner = new DirectoryScanner();

				scanner.setBasedir(resource.getDirectory());
				List<String> allIncludes = new ArrayList<String>();
				if (resource.getIncludes() != null
						&& !resource.getIncludes().isEmpty()) {
					allIncludes.addAll(resource.getIncludes());
				}
				if (includes != null && !includes.isEmpty()) {
					allIncludes.addAll(includes);
				}

				if (allIncludes.isEmpty()) {
					scanner.setIncludes(DEFAULT_INCLUDES);
				} else {
					scanner.setIncludes((String[]) allIncludes
							.toArray(EMPTY_STRING_ARRAY));
				}

				List<String> allExcludes = new ArrayList<String>();
				if (resource.getExcludes() != null
						&& !resource.getExcludes().isEmpty()) {
					allExcludes.addAll(resource.getExcludes());
				} else if (excludes != null && !excludes.isEmpty()) {
					allExcludes.addAll(excludes);
				}

				scanner.setExcludes((String[]) allExcludes
						.toArray(EMPTY_STRING_ARRAY));

				scanner.addDefaultExcludes();
				scanner.scan();

				List<String> includedFiles = Arrays.asList(scanner
						.getIncludedFiles());
				for (Iterator<String> j = includedFiles.iterator(); j.hasNext();) {
					String name = (String) j.next();
					File source = new File(resource.getDirectory(), name);
					String basename = L10n.findBasename(name);
					String localeName = L10n.findLocale(name);
					L10n l10n = l10ns.get(basename);
					if (l10n == null) {
						l10n = new L10n(basename, prj);
						l10ns.put(basename, l10n);
					}
					L10nFile l10nFile = new L10nFile(localeName, source);
					l10n.addFile(localeName, l10nFile);
					getLog().debug(
							"Adding file " + l10nFile.getSource().getName());
					try {
						l10nFile.readProperties();
					} catch (IOException e) {
						getLog().warn(
								"unable to read properties from "
										+ l10nFile.getSource()
												.getAbsolutePath());
						getLog().debug(e);
					}
				}
			}
		}
		// calculate key and missing key count for each file and localization,
		// and also totals for each locale
		calculateLocales();
		keyTotals = new int[localeStrings.size()];
		missingKeyTotals = new int[localeStrings.size()];
		for (L10n l10n : l10ns.values()) {
			l10n.calculateStatus();
			int index = 0;
			for (String locale : localeStrings) {
				L10nFile l10nFile = l10n.getFiles().get(locale);
				if (l10nFile != null) {
					keyTotals[index] += l10nFile.getKeyCount();
					missingKeyTotals[index] += l10nFile.getMissingKeyCount();
				} else {
					missingKeyTotals[index] += l10n.getKeys().size();
				}
				index++;
			}
		}

		renderer = new L10NStatusRenderer(this, getSink(),
				getBundle(reportLocale), reportLocale);
		renderer.render();
	}

	protected void calculateLocales() {
		Set<String> localeStringSet = new HashSet<String>();
		// if there are no locales configured, list all found locales
		if (locales == null) {
			getLog().debug(
					"No locales defined, using all locales found in filenames.");
			if (l10ns.values().size() > 0) {
				locales = new ArrayList<String>();
				for (L10n l10n : l10ns.values()) {
					locales.addAll(l10n.getFiles().keySet());
				}
			}
		}
		// replace null locales with empty locales to enable sorting
		for (String locale : locales) {
			if (locale != null) {
				localeStringSet.add(locale);
			}else {
				localeStringSet.add("");
			}
		}
		localeStrings = new ArrayList<String>(localeStringSet);
		Collections.sort(localeStrings);
		StringBuffer stringBuffer = new StringBuffer();
		stringBuffer.append("Using locales: ");
		for (String localeString : localeStrings) {
			stringBuffer.append(localeString + ",");
		}
		getLog().debug(stringBuffer);
	}

	/**
	 * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
	 */
	public String getDescription(Locale locale) {
		return getBundle(locale).getString("report.l10n.description");
	}

	/**
	 * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
	 */
	public String getName(Locale locale) {
		return getBundle(locale).getString("report.l10n.name");
	}

	/**
	 * @see org.apache.maven.reporting.MavenReport#getOutputName()
	 */
	public String getOutputName() {
		return "l10n-status";
	}

	private static ResourceBundle getBundle(Locale locale) {
		return ResourceBundle.getBundle("l10n-status-report", locale,
				L10NStatusReport.class.getClassLoader());
	}

	public Map<String, L10n> getL10ns() {
		return l10ns;
	}

	public void setSiteRenderer(Renderer siteRenderer) {
		this.siteRenderer = siteRenderer;
	}

	public void setProject(MavenProject project) {
		this.project = project;
	}

	public void setResources(List<Resource> resources) {
		this.resources = resources;
	}

	public void setOutputDirectory(File outputDirectory) {
		this.outputDirectory = outputDirectory;
	}

	/**
	 * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer()
	 */
	protected Renderer getSiteRenderer() {
		return siteRenderer;
	}

	/**
	 * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory()
	 */
	protected String getOutputDirectory() {
		return outputDirectory.getAbsolutePath();
	}

	/**
	 * @see org.apache.maven.reporting.AbstractMavenReport#getProject()
	 */
	protected MavenProject getProject() {
		return project;
	}

	public int[] getKeyTotals() {
		return keyTotals;
	}

	public int[] getMissingKeyTotals() {
		return missingKeyTotals;
	}

	public boolean isDetails() {
		return details;
	}

	public void setDetails(boolean details) {
		this.details = details;
	}

	public List<String> getLocaleStrings() {
		return localeStrings;
	}

	public List<MavenProject> getReactorProjects() {
		return reactorProjects;
	}

	public void setReactorProjects(List<MavenProject> reactorProjects) {
		this.reactorProjects = reactorProjects;
	}

	public boolean isAggregate() {
		return aggregate;
	}

	public void setAggregate(boolean aggregate) {
		this.aggregate = aggregate;
	}

}
